Completed
Push — master ( 318007...44d532 )
by Esaú
01:46
created

hierarchy-helper.js ➔ instanceDefinedOrNull   C

Complexity

Conditions 10
Paths 4

Size

Total Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 10
c 2
b 0
f 0
nc 4
dl 0
loc 22
rs 6.1368
nop 8

How to fix   Complexity    Many Parameters   

Complexity

Complex classes like hierarchy-helper.js ➔ instanceDefinedOrNull often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
// spec/helpers/hierarchy-helper.js
2
"use strict";
3
4
// :: DEPENDENCIES
5
6
const path = require("path");
7
const root = path.dirname(path.dirname(__dirname));
8
9
module.exports = (hierarchy) => {
10
    // check parameters
11
    if (!Array.isArray(hierarchy)) {
12
        throw new Error("hierarchy must be an array");
13
    }
14
15
    // load dependencies
16
    let deps      = hierarchy.map(value => require(path.join(root, value + ".js")));
17
    let klassName = hierarchy.pop();
18
    let Klass     = deps.pop();
19
    const first   = (hierarchy.length === 0);
20
    const third   = (hierarchy.length >= 3);
21
22
    // :: TESTING
23
24
    // test the last class in the hierarchy tree
25
    describe(klassName, () => {
26
27
        // :: INHERITED PROTOTYPE
28
29
        // all inherit from Object
30
        it("should inherit from 'Object'", () => {
31
            expect(new Klass()).toEqual(jasmine.any(Object));
32
        });
33
34
        // check the hierarchy tree
35
        for (let i = 0; i < hierarchy.length; i += 1) {
36
            it("should inherit from '" + hierarchy[i] + "'", () => {
37
                expect(new Klass()).toEqual(jasmine.any(deps[i]));
38
            });
39
        }
40
41
        // check inherited properties
42
        if (!first) {
43
            it("should have a prototype property named 'name'", () => {
44
                expect(Klass.prototype).toHaveString("name");
45
            });
46
47
            it("should have a prototype property named 'message'", () => {
48
                expect(Klass.prototype).toHaveString("message");
49
            });
50
51
            it("should have a prototype property named 'code'", () => {
52
                expect(Klass.prototype).toHaveMember("code");
53
            });
54
        }
55
56
        // check Object methods
57
        it("should have a prototype method named 'toString()'", () => {
58
            expect(Klass.prototype).toHaveMethod("toString");
59
        });
60
61
        // check inherited methods
62
        if (!first) {
63
            it("should have a prototype method named 'native()'", () => {
64
                expect(Klass.prototype).toHaveMethod("native");
65
            });
66
        }
67
68
        // :: EXTENDED PROTOTYPE
69
70
        // check extended properties
71
        if (first) {
72
            it("should have a prototype property named 'name'", () => {
73
                expect(Klass.prototype).toHaveString("name");
74
            });
75
76
            it("should have a prototype property named 'message'", () => {
77
                expect(Klass.prototype).toHaveString("message");
78
            });
79
80
            it("should have a prototype property named 'code'", () => {
81
                expect(Klass.prototype).toHaveMember("code");
82
            });
83
84
            it("should have a prototype method named 'native()'", () => {
85
                expect(Klass.prototype).toHaveMethod("native");
86
            });
87
        }
88
89
        // :: PROTOTYPE VALUES
90
91
        it("should have the 'class' name in the prototype property named 'name'", () => {
92
            expect(Klass.prototype.name).toEqual(klassName);
93
        });
94
95
        it("should have a dummy default value as message", () => {
96
            expect(Klass.prototype.message).toEqual("thrown");
97
        });
98
99
        it("should have a null default value as code", () => {
100
            expect(Klass.prototype.code).toBeNull();
101
        });
102
103
        // :: CONSTRUCTOR
104
105
        it("should instantiate without parameters", () => {
106
            instanceNoParameters(Klass, testNoErrors, third);
107
            testNoErrors(() => new Klass());
108
        });
109
110
        it("should instantiate with parameters", () => {
111
            instanceParameters(Klass, testNoErrors, testNoErrors, testNoErrors, third);
112
        });
113
114
        // use the tests according to the hierarchy level
115
        if (third) {
116
            testThird(Klass);
117
        } else {
118
            test(Klass);
119
        }
120
121
    });
122
123
};
124
125
// Tests that a function doesn't throw any Error
126
function testNoErrors(fn) {
127
    expect(fn).not.toThrowError("parameter 'name' must be a 'string'");
128
    expect(fn).not.toThrowError("parameter 'message' must be a 'string'");
129
    expect(fn).not.toThrowError("parameter 'code' must be a 'number'");
130
}
131
132
// Tests instantiation of a class without parameters (undefined, null or none).
133
function instanceNoParameters(Klass, fn, third) {
134
    let arg1, arg2, arg3, test;
135
    test = (() => new Klass(arg1, arg2, arg3));
136
    for (let i = 0; i < 2; i += 1) {
137
        arg1 = (i % 2 === 0 ? undefined : null);
138
        for (let j = 0; j < 2; j += 1) {
139
            arg2 = (j % 2 === 0 ? undefined : null);
140
            if (third) {
141
                fn(test);
142
            } else {
143
                for (let e = 0; e < 2; e += 1) {
144
                    arg3 = (e % 2 === 0 ? undefined : null);
145
                    fn(test);
146
                }
147
            }
148
        }
149
    }
150
}
151
152
// Tests instantiation of a class with parameters.
153
function instanceParameters(Klass, fn1, fn2, fn3, third) {
154
    let arg1, arg2, arg3, test3, args1, args2, args3;
155
    const test1 = (() => new Klass(arg1));
156
    const test2 = (() => new Klass(arg1, arg2));
157
    if (third) {
158
        test3 = (() => null);
159
        args1 = [undefined, null, Klass.prototype.message];
160
        args2 = [undefined, null, Math.round(Math.random() * 0xFFFFFFFF)];
161
        args3 = [];
162
    } else {
163
        test3 = (() => new Klass(arg1, arg2, arg3));
164
        args1 = [undefined, null, Klass.prototype.name];
165
        args2 = [undefined, null, Klass.prototype.message];
166
        args3 = [undefined, null, Math.round(Math.random() * 0xFFFFFFFF)];
167
    }
168
    for (let i = 0; i < args1.length; i += 1) {
169
        arg1 = args1[i];
170
        fn1(test1);
171
        for (let j = 0; j < args2.length; j += 1) {
172
            arg2 = args2[j];
173
            fn2(test2);
174
            if (!third) {
175
                for (let e = 0; e < args3.length; e += 1) {
176
                    arg3 = args3[e];
177
                    fn3(test3);
178
                }
179
            }
180
        }
181
    }
182
}
183
184
// Loops for each parameter of the Klass constructor.
185
// If the iteration is even, the parameter is defined.
186
// If the iteration is odd, the parameter is null.
187
function instanceDefinedOrNull(Klass, name, message, code, fn1, fn2, fn3, third) {
188
    for (let i = 0; i < 2; i += 1) {
189
        const even1   = (i % 2 === 0);
190
        const arg1    = (even1 ? (third ? message : name) : null);
191
        const source1 = new Klass(arg1);
192
        fn1(source1, even1);
193
        for (let j = 0; j < 2; j += 1) {
194
            const even2   = (j % 2 === 0);
195
            const arg2    = (even2 ? (third ? code : message) : null);
196
            const source2 = new Klass(arg1, arg2);
197
            fn2(source2, even1, even2);
198
            if (!third) {
199
                for (let e = 0; e < 2; e += 1) {
200
                    const even3   = (e % 2 === 0);
201
                    const arg3    = (even3 ? code : null);
202
                    const source3 = new Klass(arg1, arg2, arg3);
203
                    fn3(source3, even1, even2, even3);
204
                }
205
            }
206
        }
207
    }
208
}
209
210
// Loops for each parameter of the Klass constructor using wrong types to test Error throwing.
211
function instanceThrowErrors(Klass, fn1, fn2, fn3, third) {
212
    let arg1, arg2, arg3, test33, test32, test31, test21, test22, test11, len1, len2, len3;
213
    const noStr = [{}, true, false, 42, 3.1416, -42, -3.1416, () => null];
214
    const noNmb = [{}, true, false, '', "qwerty", () => null];
215
    len1        = noStr.length;
216
    if (third) {
217
        len2   = noNmb.length;
218
        len3   = 0;
219
        test33 = test32 = test31 = (() => null);
220
    } else {
221
        len2   = len1;
222
        len3   = noNmb.length;
223
        test33 = (() => new Klass(arg1, arg2, arg3));
224
        test32 = (() => new Klass(null, arg2, arg3));
225
        test31 = (() => new Klass(null, null, arg3));
226
    }
227
    test22 = (() => new Klass(arg1, arg2));
228
    test21 = (() => new Klass(null, arg2));
229
    test11 = (() => new Klass(arg1));
230
    if (typeof Symbol === "function") {
231
        noStr.push(Symbol("symbol"));
232
        noNmb.push(Symbol("symbol"));
233
    }
234
    for (let i = 0; i < len1; i += 1) {
235
        arg1 = noStr[i];
236
        fn1(test11);
237
        for (let j = 0; j < len2; j += 1) {
238
            arg2 = (third ? noNmb[j] : noStr[j]);
239
            fn2(test21, test22);
240
            if (!third) {
241
                for (let e = 0; e < len3; e += 1) {
242
                    arg3 = noNmb[e];
243
                    fn3(test31, test32, test33);
244
                }
245
            }
246
        }
247
    }
248
}
249
250
// Tests classes that are a third level subclass, meaning that they require 2 arguments (message and code).
251
function testThird(Klass) {
252
253
    // :: CONSTRUCTOR
254
255
    it("should throw an Error if 'message' or 'code' are invalid parameters", () => {
256
        instanceThrowErrors(Klass, (test11) => {
257
            expect(test11).toThrowError("parameter 'message' must be a 'string'");
258
        }, (test21, test22) => {
259
            expect(test21).toThrowError("parameter 'code' must be a 'number'");
260
            expect(test22).toThrowError("parameter 'message' must be a 'string'");
261
        }, null, true);
262
    });
263
264
    // :: MEMBER PROPERTIES
265
266
    const message = "asdf";
267
    const code    = Math.round(Math.random() * 0xFFFFFFFF);
268
269
    it("should have all correct properties once instantiated", () => {
270
        instanceDefinedOrNull(Klass, null, message, code, (instance, even) => {
271
            if (even) {
272
                expect(instance.name).toEqual(Klass.prototype.name);
273
                expect(instance.message).toEqual(message);
274
            } else {
275
                expect(instance.name).toEqual(Klass.prototype.name);
276
                expect(instance.message).toEqual(Klass.prototype.message);
277
            }
278
            expect(instance.code).toBeNull();
279
        }, (instance, even1, even2) => {
280
            expect(instance.name).toEqual(Klass.prototype.name);
281
            expect(instance.message).toEqual(even1 ? message : Klass.prototype.message);
282
            expect(instance.code).toEqual(even2 ? code : null);
283
        }, null, true);
284
    });
285
286
    // :: MEMBER METHODS
287
288
    it("#toString()", () => {
289
        instanceDefinedOrNull(Klass, null, message, code, (instance, even) => {
290
            let exp;
291
            if (even) {
292
                exp = Klass.prototype.name + ": " + message + '.';
293
            } else {
294
                exp = Klass.prototype.name + ": " + Klass.prototype.message + '.';
295
            }
296
            expect(instance.toString()).toEqual(exp);
297
        }, (instance, even1, even2) => {
298
            let exp;
299
            exp  = Klass.prototype.name;
300
            exp += (even2 ? " (0x" + code.toString(16) + "):" : ':' ) + ' ';
301
            exp += (even1 ? message : Klass.prototype.message) + '.';
302
            expect(instance.toString()).toEqual(exp);
303
        }, null, true);
304
    });
305
306
    it("#native()", () => {
307
        instanceDefinedOrNull(Klass, null, message, code, (instance, even) => {
308
            const exp = (even ? message : Klass.prototype.message);
309
            expect(instance.native()).toEqual(new Error(exp));
310
        }, (instance, even1) => {
311
            const exp = (even1 ? message : Klass.prototype.message);
312
            expect(instance.native()).toEqual(new Error(exp));
313
        }, null, true);
314
    });
315
316
}
317
318
// Tests classes that require 3 arguments (name, message and code).
319
function test(Klass) {
320
321
    // :: CONSTRUCTOR
322
323
    it("should throw an Error if 'message' or 'code' are invalid parameters", () => {
324
        instanceThrowErrors(Klass, (test11) => {
325
            expect(test11).toThrowError("parameter 'name' must be a 'string'");
326
        }, (test21, test22) => {
327
            expect(test22).toThrowError("parameter 'name' must be a 'string'");
328
            expect(test21).toThrowError("parameter 'message' must be a 'string'");
329
        }, (test31, test32, test33) => {
330
            expect(test33).toThrowError("parameter 'name' must be a 'string'");
331
            expect(test32).toThrowError("parameter 'message' must be a 'string'");
332
            expect(test31).toThrowError("parameter 'code' must be a 'number'");
333
        }, false);
334
    });
335
336
    // :: MEMBER PROPERTIES
337
338
    const name    = "qwerty";
339
    const message = "asdf";
340
    const code    = Math.round(Math.random() * 0xFFFFFFFF);
341
342
    it("should have all correct properties once instantiated", () => {
343
        instanceDefinedOrNull(Klass, name, message, code, (instance, even) => {
344
            if (even) {
345
                expect(instance.name).toEqual(name);
346
                expect(instance.message).toEqual(Klass.prototype.message);
347
            } else {
348
                expect(instance.name).toEqual(Klass.prototype.name);
349
                expect(instance.message).toEqual(Klass.prototype.message);
350
            }
351
            expect(instance.code).toBeNull();
352
        }, (instance, even1, even2) => {
353
            expect(instance.name).toEqual(even1 ? name : Klass.prototype.name);
354
            expect(instance.message).toEqual(even2 ? message : Klass.prototype.message);
355
            expect(instance.code).toBeNull();
356
        }, (instance, even1, even2, even3) => {
357
            expect(instance.name).toEqual(even1 ? name : Klass.prototype.name);
358
            expect(instance.message).toEqual(even2 ? message : Klass.prototype.message);
359
            expect(instance.code).toEqual(even3 ? code : null);
360
        }, false);
361
    });
362
363
    // :: MEMBER METHODS
364
365
    it("#toString()", () => {
366
        instanceDefinedOrNull(Klass, name, message, code, (instance, even) => {
367
            let exp;
368
            if (even) {
369
                exp = name + ": " + Klass.prototype.message + '.';
370
            } else {
371
                exp = Klass.prototype.name + ": " + Klass.prototype.message + '.';
372
            }
373
            expect(instance.toString()).toEqual(exp);
374
        }, (instance, even1, even2) => {
375
            let exp;
376
            exp  = (even1 ? name : Klass.prototype.name) + ':';
377
            exp += ' ' + (even2 ? message : Klass.prototype.message) + '.';
378
            expect(instance.toString()).toEqual(exp);
379
        }, (instance, even1, even2, even3) => {
380
            let exp;
381
            exp  = (even1 ? name : Klass.prototype.name);
382
            exp += (even3 ? " (0x" + code.toString(16) + "):" : ':') + ' ';
383
            exp += (even2 ? message : Klass.prototype.message) + '.';
384
            expect(instance.toString()).toEqual(exp);
385
        }, false);
386
    });
387
388
    it("#native()", () => {
389
        instanceDefinedOrNull(Klass, name, message, code, (instance) => {
390
            const exp = Klass.prototype.message;
391
            expect(instance.native()).toEqual(new Error(exp));
392
        }, (instance, even1, even2) => {
393
            const exp = (even2 ? message : Klass.prototype.message);
394
            expect(instance.native()).toEqual(new Error(exp));
395
        }, (instance, even1, even2) => {
396
            const exp = (even2 ? message : Klass.prototype.message);
397
            expect(instance.native()).toEqual(new Error(exp));
398
        }, false);
399
    });
400
401
}